今天主要是來分享目前 Messenger UI 克隆計畫的進度
目前仍然是著重 UI 的呈現上
雖然許多細節還有很多不同就是了 Orz
但主要想呈現出來的畫面效果還可以接受
今天就著重分享我完成了哪些效果吧!
完成大綱畫面 (2/3)
重構程式碼 (x) <- 這個叉應該是整個專案進行中都要持續存在的
透過 BottomNavigationBar 進行 Navigator(x)
透過 GridView 來達到平均切割畫面的效果 (x)
透過 Stack 來進行畫面堆疊 (x)
使用套件 username_gen 來產生隨機的英文名字 (x)
實作隨機顏色產生 (x)
首先讓我們先重構前天的程式碼~
建立 HomeScreen.dart
裡頭主要將後面用到的 Scafford 提到這一層來做呈現
並且透過 BottomNavigatorBar 進行畫面切換~
這邊可以注意到有些變數名稱是用 _
來命名
原因是 Dart 不像其他具備物件導向特性的語言(ex. C++、C#、Java...)有 public
、private
等關鍵字
而在 Dart 則透過在變數或方法前面加上 _
來表示其為私有變數或方法
// 篇幅的關係有刪除某些內容,完整請查閱原始碼
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
late int _currentIndex = 0;
late String _title = title(0);
late List<Widget> _actions = [];
late Widget? _leading;
@override
void initState() {
super.initState();
setState(() {
_title = title(_currentIndex);
_actions = actions(_currentIndex);
_leading = leading(_currentIndex);
});
}
void _onItemTapped(int index) {
setState(() {
_currentIndex = index;
_title = title(_currentIndex);
_actions = actions(_currentIndex);
_leading = leading(_currentIndex);
});
}
// Get body's content
Widget content(int index) {
late Widget result;
switch (index) {
case 0:
result = const ChatRoomScreen();
break;
case 1:
result = const UsersScreen();
break;
case 2:
result = const SettingScreen();
break;
default:
throw Exception("No Page in this index.");
}
return result;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(40),
child: AppBar(
leading: _leading,
title: Text(_title),
actions: _actions,
),
),
body: content(_currentIndex),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(icon: Icon(Icons.message), label: "聊天室"),
BottomNavigationBarItem(icon: Icon(Icons.people), label: "用戶"),
BottomNavigationBarItem(
icon: CircleAvatar(
radius: 15,
backgroundImage: NetworkImage(
"https://pbs.twimg.com/profile_images/1187814172307800064/MhnwJbxw_400x400.jpg"),
),
label: "設定"),
],
currentIndex: _currentIndex,
onTap: _onItemTapped,
),
);
}
}
在上面完成畫面切換後
讓我們來具體實作 UsersScreen 吧~
這部分用到的材料(Widgets)有
首先在實作的時,我們先從大項開始完成
透過 DefaultTabController
我們首先完成了用戶限時動態以及線上用戶兩個畫面的切換
此時需要配合 TabBar
以及 Tab
來完成
前者用來設計 Bar 上面的文字、顏色、字體... 等相關參數
後者則是可以用來呈現切換後的畫面
在這邊我們透過宣告回傳值為 Widget 的兩個函式 story()
以及 onlineUsers()
來完成這兩個畫面
在 story 中
我們透過 GridView 來將畫面分割出來
此時相關 間隔
及 幾個一組
等參數則是透過 SliverGridDelegateWithFixedCrossAxisCount
來完成
而接下來我這邊透過 Stack
來完成畫面上的堆疊效果
最底下我們用 ClipRRect
來產生一個具備圓弧邊框的圖片
而在這之上我們加上了三個 Widgets
分別是 ElevatedButton
、Spacer
以及 Text
Spacer 在這邊的用途是將兩個元件隔開達到上下 Widgets 位於邊界兩端的效果
而在 onlineUesrs 中
我們則透過簡易的 LiseView 達到可以捲動畫面的效果
而為了不這麼單調 我在這邊使用了 username_gen
這個套件來隨機產生英文人名
並透過隨機的調色來讓大頭貼的背景顏色不會重複(至少看起來比較開心~)
class UsersScreen extends StatefulWidget {
const UsersScreen({super.key});
@override
State<UsersScreen> createState() => _UsersScreenState();
}
class _UsersScreenState extends State<UsersScreen> {
Widget story() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
child: GridView(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.7,
mainAxisSpacing: 10,
crossAxisSpacing: 10),
children: [
for (int i = 0; i < 20; i++) const StoryCard(),
]),
);
}
Widget onlineUsers() {
return ListView(
children: [
const ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(
"https://pbs.twimg.com/profile_images/1455185376876826625/s1AjSxph_400x400.jpg")),
title: Text("Google"),
),
const ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(
"https://pbs.twimg.com/profile_images/1187814172307800064/MhnwJbxw_400x400.jpg")),
title: Text("Flutter"),
),
for (int i = 0; i < 49; i++)
ListTile(
leading: CircleAvatar(
backgroundColor: Color(Random.secure().nextInt(16777215) |
0xFF000000), // generate random color
child: Text("F$i"),
),
title: Text(UsernameGen().generate()),
),
],
);
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Column(
children: <Widget>[
const TabBar(
indicatorSize: TabBarIndicatorSize.label,
indicatorWeight: 1,
labelColor: Colors.black,
labelStyle: TextStyle(fontSize: 12),
tabs: <Widget>[
Tab(
text: "限時動態",
),
Tab(text: "在線上")
],
),
Expanded(
child: TabBarView(
children: <Widget>[story(), onlineUsers()],
),
),
],
),
);
}
}
class StoryCard extends StatelessWidget {
const StoryCard({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(15),
child: Image.network(
"https://blogger.googleusercontent.com/img/a/AVvXsEiBvTaWkOFFihJud4ctimi-3DXWWjwU_x98aUPlba97hoBkHFASSExnr4U5JatHKG_PTDVeyDJ37dPC1EbAtGLNPZP9ixKznYdrTee8cs8kEiiiDfFdHUJ3JDMg2rGLCDCsmYMxSKzq7ci_PrWr4UEuPW1I5VVPTOHY282HjbC4AU5tCVqnvsu-Ss3p",
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.grey,
backgroundColor: Colors.white,
shape: const CircleBorder(), // <-- Splash color
),
onPressed: () => {},
child: const Icon(
Icons.add,
color: Colors.black,
),
),
const Spacer(),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
"新增到限時動態",
style: TextStyle(color: Colors.white),
),
)
],
),
)
],
);
}
}
網格排列的 GridView
username_gen
《Flutter实战·第二版》